S3 バケット作成直後に署名付き URL を発行したら SignatureDoesNotMatch になるが、時間の経過で解決する事象について
こんにちは!AWS 事業本部コンサルティング部のたかくに(@takakuni_)です。
タイトルが長いですが、先日 S3 バケットを作成し署名付き URL を発行することがありました。その際にタイトルの事象に遭遇しまして 2 日詰まりました。
そこで今回はなぜ発生したのかまとめてみたいと思います。
S3 バケットの DNS ルーティングについて
S3 バケットの DNS ルーティングについておさらいします。
対象の S3 バケットを操作するために、エンドポイント(johnsmith.s3.amazonaws.com
)の名前解決を行います。(1)
DNS サーバーは対象エンドポイントをホストしている施設(今回だと施設 B)の IP を応答します。(2)
クライアントは対象 IP アドレスに対してリクエスト送り、S3 の操作を行います。(3,4)
ここまではいつもの DNS のやり取りかと思います。
バージニアリージョン以外の S3 バケットを作成した直後の API リクエストについて
ここからが本題です。S3 バケット johnsmith
が東京リージョンで作成されたと仮定します。
バージニアリージョン以外の S3 バケットのエンドポイント(johnsmith.s3.amazonaws.com
)は作成後、 DNS 伝播に時間を要します。(S3 バケット johnsmith
が東京リージョンにいることを DNS サーバーに伝播するのに時間を要します。)
そのため、クライアントから johnsmith.s3.amazonaws.com
にアクセスした際に DNS 伝播が完了していない場合は DNS サーバーはデフォルトエンドポイント(s3.amazonaws.com
)を応答します。(2)
クライアントはデフォルトエンドポイントにアクセスしますが、デフォルトエンドポイントは東京リージョンのエンドポイント(s3.ap-northeast-1.amazonaws.com
)へのリダイレクトを返答します。(3,4)
クライアントはようやく S3 のやり取りを東京リージョンのエンドポイントとやり取りできます。(5,6)
この辺りの制御は、以下のドキュメントに記載されているため、興味のある方は是非ご覧ください。
私が発生した事象
DNS 伝播が済んでいない状態でのデフォルトエンドポイントのリダイレクトが原因でした。
リダイレクトが生じたことで、署名付き URL のホスト名(johnsmith.s3.amazonaws.com
)とリダイレクト先のホスト名(johnsmith.s3.ap-northeast-1.amazonaws.com
)が異なり、 SignatureDoesNotMatch のエラーが発生していました。
SignatureDoesNotMatch の一般的なトラブルシューティングには以下のドキュメントが参考になりますが、今回のような時間経過で解決するパターンははじめてで非常に勉強になりました。
再現してみる
実際に事象を再現するのが、イメージつきやすいと思うので、サンプルコードを用意してみました。
中身はシンプルで S3 のイベント通知をトリガーに URL を生成、CloudWatch Logs に記録するといった構成です。
Lambda のコードは以下になります。
import os
import json
import logging
import urllib.parse
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError
logger = logging.getLogger()
region = os.environ['AWS_REGION']
s3 = boto3.client(
's3',
region_name=region,
config=Config(signature_version='s3v4')
)
def lambda_handler(event, context):
try:
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
url = s3.generate_presigned_url(
ClientMethod = 'get_object',
Params = {'Bucket' : bucket, 'Key' : key},
ExpiresIn = 3600,
HttpMethod = 'GET'
)
logger.info("Got presigned URL: %s", url)
except ClientError:
logger.exception(
"Couldn't get a presigned URL."
)
raise
return url
生成された URL をみてみます。s3.amazonaws.com
から始まっていますね。
https://presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com/input/[FILE_NAME].html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=[MASKED_CREDENTIAL]%2F[DATE]%2F[REGION]%2Fs3%2Faws4_request&X-Amz-Date=[DATE]T[TIME]Z&X-Amz-Expires=[EXPIRATION]&X-Amz-SignedHeaders=host&X-Amz-Security-Token=[MASKED_TOKEN]&X-Amz-Signature=[MASKED_SIGNATURE]
curl でアクセスしてみましょう。
s3.amazonaws.com
から s3-ap-northeast-1.amazonaws.com
へのリダイレクト(307)が発生したのちに、403 エラーが発生しています。
takakuni@ cause-dns-SignatureDoesNotMatch % curl -o takakuni.html "https://presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com/input/takakuni.html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=[MASKED-CREDENTIAL]&X-Amz-Date=20241227T135505Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=[MASKED-TOKEN]&X-Amz-Signature=[MASKED-SIGNATURE]" -v -L
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
* Host presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com:443 was resolved.
* IPv6: (none)
* IPv4: [MASKED-IPS]
* Trying [MASKED-IP]:443...
* Connected to presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com ([MASKED-IP]) port 443
* ALPN: curl offers h2,http/1.1
[TLS handshake details omitted]
* SSL connection using TLSv1.3 / AEAD-AES128-GCM-SHA256
* ALPN: server accepted http/1.1
* Server certificate:
* subject: CN=*.s3.amazonaws.com
* start date: Apr 22 00:00:00 2024 GMT
* expire date: Apr 7 23:59:59 2025 GMT
* subjectAltName: host "presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com" matched cert's "*.s3.amazonaws.com"
* issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M01
* SSL certificate verify ok.
* using HTTP/1.x
> GET /input/takakuni.html?[PRESIGNED-URL-PARAMS] HTTP/1.1
> Host: presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 307 Temporary Redirect
< x-amz-bucket-region: ap-northeast-1
< x-amz-request-id: [MASKED-REQUEST-ID]
< x-amz-id-2: [MASKED-ID]
< Location: https://presigned-lambda-[ACCOUNT_ID].s3-ap-northeast-1.amazonaws.com/[MASKED-REDIRECT-URL]
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Date: Fri, 27 Dec 2024 14:01:55 GMT
< Server: AmazonS3
[Second request details with similar masking...]
< HTTP/1.1 403 Forbidden
< x-amz-request-id: [MASKED-REQUEST-ID]
< x-amz-id-2: [MASKED-ID]
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Date: Fri, 27 Dec 2024 14:01:55 GMT
< Server: AmazonS3
取得した HTML ファイルも SignatureDoesNotMatch と記載されていますね。
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<code>SignatureDoesNotMatch</code>
<Message
>The request signature we calculated does not match the signature you
provided. Check your key and signing method.</Message
>
<AWSAccessKeyId>[MASKED-ACCESS-KEY]</AWSAccessKeyId>
<StringToSign
>AWS4-HMAC-SHA256 20241227T135505Z 20241227/ap-northeast-1/s3/aws4_request
[MASKED-STRING-TO-SIGN-HASH]</StringToSign
>
<SignatureProvided>[MASKED-SIGNATURE]</SignatureProvided>
<StringToSignBytes>[MASKED-BYTES]</StringToSignBytes>
<CanonicalRequest
>GET /input/takakuni.html [MASKED-CANONICAL-REQUEST-PARAMS]
host:presigned-lambda-[ACCOUNT_ID].s3-ap-northeast-1.amazonaws.com host
UNSIGNED-PAYLOAD</CanonicalRequest
>
<CanonicalRequestBytes
>[MASKED-CANONICAL-REQUEST-BYTES]</CanonicalRequestBytes
>
<RequestId>[MASKED-REQUEST-ID]</RequestId>
<HostId>[MASKED-HOST-ID]</HostId>
</Error>
リージョナルエンドポイントで発行する
上記の通り DNS 伝播が済めば(時間が経てば)、リダイレクトされることなく到達し解決するのですが、恒久的に解決したくなる方もいるのではないでしょうか。その場合、署名付き URL の発行形式(addressing_style
)を virtual に変更することで解決ができる可能性があります。
import os
import json
import logging
import urllib.parse
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError
logger = logging.getLogger()
region = os.environ['AWS_REGION']
s3 = boto3.client(
's3',
region_name=region,
- config=Config(signature_version='s3v4')
+ config=Config(signature_version='s3v4', s3={'addressing_style': 'virtual'})
)
def lambda_handler(event, context):
try:
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
url = s3.generate_presigned_url(
ClientMethod = 'get_object',
Params = {'Bucket' : bucket, 'Key' : key},
ExpiresIn = 3600,
HttpMethod = 'GET'
)
logger.info("Got presigned URL: %s", url)
except ClientError:
logger.exception(
"Couldn't get a presigned URL."
)
raise
return url
上記の処理を行うことで署名付き URL が s3.amazonaws.com
ではなく s3.ap-northeast-1.amazonaws.com
で発行されます。
https://presigned-lambda-[ACCOUNT_ID].s3.ap-northeast-1.amazonaws.com/input/[FILE_NAME].html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=[MASKED_CREDENTIAL]%2F[DATE]%2F[REGION]%2Fs3%2Faws4_request&X-Amz-Date=[DATE]T[TIME]Z&X-Amz-Expires=[EXPIRATION]&X-Amz-SignedHeaders=host&X-Amz-Security-Token=[MASKED_TOKEN]&X-Amz-Signature=[MASKED_SIGNATURE]
同じく curl の実行結果です。こちらは 200 OK となりました。
curl -o [FILE_NAME].html "https://presigned-lambda2-[ACCOUNT_ID].s3.ap-northeast-1.amazonaws.com/input/[FILE_NAME].html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=[MASKED_CREDENTIAL]%2F[DATE]%2F[REGION]%2Fs3%2Faws4_request&X-Amz-Date=[DATE]T[TIME]Z&X-Amz-Expires=[EXPIRATION]&X-Amz-SignedHeaders=host&X-Amz-Security-Token=[MASKED_TOKEN]&X-Amz-Signature=[MASKED_SIGNATURE]" -v -L
* Host presigned-lambda2-[ACCOUNT_ID].s3.ap-northeast-1.amazonaws.com:443 was resolved.
* IPv4: [MASKED_IPS]
* Connected to presigned-lambda2-[ACCOUNT_ID].s3.ap-northeast-1.amazonaws.com ([MASKED_IP]) port 443
[SSL/TLS connection details masked]
* Server certificate:
* subject: CN=*.s3-ap-northeast-1.amazonaws.com
* start date: [MASKED_DATE]
* expire date: [MASKED_DATE]
* subjectAltName: host "[MASKED_HOST]" matched cert's "[MASKED_CERT]"
* issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M01
< HTTP/1.1 200 OK
< x-amz-id-2: [MASKED_ID]
< x-amz-request-id: [MASKED_REQUEST_ID]
< Date: [MASKED_DATE]
< Last-Modified: [MASKED_DATE]
< x-amz-expiration: expiry-date="[MASKED_DATE]", rule-id="[RULE_ID]"
< ETag: [MASKED_ETAG]
< x-amz-server-side-encryption: AES256
< Content-Type: application/octet-stream
< Content-Length: [LENGTH]
< Server: AmazonS3
{ [64 bytes data]
100 64 100 64 0 0 423 0 --:--:-- --:--:-- --:--:-- 426
* Connection #0 to host presigned-lambda2-622809842341.s3.ap-northeast-1.amazonaws.com left intact
addressing_style が virtual でも通用しないケース
addressing_style が virtual でも通用しないケースとして、ピリオドを含むバケット名が挙げられます。
そもそも、仮想ホスト形式ではピリオドを含むバケット名がサポートされていないため、署名付き URL とは別の話だと思いますが念の為。
まとめ
以上、「S3 バケット作成直後に署名付き URL を発行したら SignatureDoesNotMatch になるが、時間の経過で解決する事象について」でした。
私はこの事象を解決するまでに 2 日もかかってしまいました。このブログがどなたかの参考になれば幸いです。
AWS 事業本部コンサルティング部のたかくに(@takakuni_)でした!